use std::fs::File;

use im_rc::HashMap;

use crate::{
    parser::Value,
    runtime::{Env, FuncBinds},
};

use super::Builtin;

pub fn bind(env: &mut FuncBinds) {
    env.bind_builtin("cmd", Cmd { opt: false });

    env.bind_builtin("cmdOpt", Cmd { opt: true });
}

struct Cmd {
    opt: bool,
}

impl Builtin for Cmd {
    fn call(&self, env: &Env, argv: &[Value]) -> Result<Value, ()> {
        let tmp_opt;

        let (opt, argv) = if self.opt {
            let Some((Value::Hash(opt), argv)) = argv.split_first() else {
                eprintln!("cmdOpt: expect as hmap as option");
                return Err(());
            };

            (opt, argv)
        } else {
            tmp_opt = HashMap::new();
            (&tmp_opt, argv)
        };

        let stdout = match opt.get("stdout") {
            Some(Value::Path(path_buf)) => Some(path_buf.to_owned()),
            Some(Value::OsString(os_string)) => Some(os_string.into()),
            Some(Value::String(string)) => Some(string.into()),
            None => None,
            _ => {
                return Err(());
            }
        };

        let Some((cmd, args)) = argv.split_first() else {
            eprintln!("cmd: no arguments");
            return Err(());
        };

        let cmd = match cmd {
            Value::Symbol(name) => name,
            Value::String(name) => name,
            val => {
                eprintln!("cmd: command name should be symbol or string instead of {val:?}");
                return Err(());
            }
        };

        let mut cmd = std::process::Command::new(cmd);

        for each in args {
            add_cmd_arg(each, &mut cmd);
        }

        if let Some(cwd) = env.get_cwd() {
            cmd.current_dir(cwd);
        }

        if let Some(stdout) = stdout {
            let writer = File::create(stdout)
                .map_err(|err| eprintln!("failed to create redirect result: {err}"))?;
            cmd.stdout(writer);
        }

        let st = cmd.status().map_err(|err| {
            eprintln!(
                "cmd: failed to run program {:?}: {}",
                cmd.get_program(),
                err
            )
        })?;

        if !st.success() {
            eprintln!(
                "cmd: program {:?} exit with {:?}",
                cmd.get_program(),
                st.code(),
            );
            return Err(());
        }

        Ok(Value::Integer(0))
    }
}

fn add_cmd_arg(each: &Value, cmd: &mut std::process::Command) {
    match each {
        Value::Symbol(s) => {
            cmd.arg(s);
        }
        Value::String(s) => {
            cmd.arg(s);
        }
        Value::OsString(os) => {
            cmd.arg(os);
        }
        Value::Bool(x) => {
            cmd.arg(x.to_string());
        }
        Value::Integer(x) => {
            cmd.arg(x.to_string());
        }
        Value::Path(p) => {
            cmd.arg(p);
        }
        Value::Array(vec) => {
            for each in vec {
                add_cmd_arg(each, cmd);
            }
        }
        Value::Hash(_) => {
            todo!()
        }
    };
}
